﻿// FILE           :ClientHandler.cs
// PROJECT        :SETCritters                                        
// PROGRAMMER     :Kenneth Jinks and Ryan Pink                                         
// FIRST VERSION  :Sept 11 2013                                                
// DESCRIPTION    :
//      This file contains the ClientHandler class, which is instantiated when a client
//      connects and processes the given client's packet requests

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Drawing;

namespace TheServer
{
    
    /// <summary>
    /// #ClientHandler
    /// 
    /// The server class uses the websocket protocol to communicate with the client.
    /// This has been referenced from 
    /// https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server
    /// The server starts a new instantiation of ClientHandler passing the TcpClient to
    /// the constructor.
    /// The static variable nextClientNumber increments once each instantiation.
    /// 
    /// all Console.WriteLines are wrapped in mutex protection so we don't slam the console
    ///
    /// Future_Tweaks
    /// -it may be possible to increase performance by creating a seperate mutex for
    /// thread access to database and thread access to the console output
    /// 
    /// -the server starts a new instantiation of ClientHandler passing the TcpClient to
    /// the constructor.
    /// 
    /// -the client number in the client handler does not need to reflect the number in the packet
    /// -the original intent was that the client would send a new command on the first connection. 
    /// the server would respond to the new command by giving a new client number to that client 
    /// and that client number would serve to keep continuity in communications
    /// 
    /// -the static variable nextClientNumber increments once each instantiation and may need to be 
    /// changed to reflect above to align with the packet client number, not the instatiation number.
    /// 
    /// </summary>
    class ClientHandler
    {
        TcpClient theClient;
        int clientNumber;

        private static Mutex theMutex = new Mutex();
        static int nextClientNumber = 0;

        /// <summary>
        /// ClientHandler - Default Constructor
        /// 
        /// Assigns the client a new client number and prepares the mutex
        /// </summary>
        public ClientHandler()
        {
            clientNumber = nextClientNumber;
            nextClientNumber++;

            theMutex.WaitOne();
            Console.WriteLine("Client[" + clientNumber.ToString() + "]: Client handler started...");
            theMutex.ReleaseMutex();
        }

        /// <summary>
        /// ~ClientHandler - Default Destructor
        /// 
        /// Gracefully closes the port for reuse
        /// </summary>
        ~ClientHandler()
        {
            theClient.Close();
        }

        /// <summary>
        /// StartClient - Captures the client connection and assigns a thread
        /// </summary>
        /// <param name="newClient">Client ID number associated with a new thread</param>
        public void StartClient(ref TcpClient newClient)
        {
            theClient = newClient;

            try
            {
                theMutex.WaitOne();
                Console.WriteLine("Client[" + clientNumber.ToString() + "]: Starting new client thread...");
                theMutex.ReleaseMutex();

                Thread clientThread = new Thread(ClientActive);
                clientThread.Start();
            }
            catch (Exception ex)
            {
                theMutex.WaitOne();
                Console.WriteLine("Client[" + clientNumber.ToString() + "]: Error establishing thread:" + ex.ToString());
                theMutex.ReleaseMutex();
            }
        }

        /// <summary>
        /// ClientActive - Completes the handshake, accepts packet information, and
        ///                sends them to the database to be processed
        /// </summary>
        public void ClientActive()
        {
            theMutex.WaitOne();
            Console.WriteLine("Client[" + clientNumber.ToString() + "]: New client thread started...");
            theMutex.ReleaseMutex();

            string fromClient = "";

            Handshake(ref theClient);   // Attempt the handshake using websocket protocols

            do
            {
                fromClient = Encoding.UTF8.GetString(               // decode the incoming packet information
                                    DecryptByteArray(
                                    ReadFromClient(ref theClient)
                                    )
                                    );
                theMutex.WaitOne();
                Console.WriteLine("Client[" + clientNumber.ToString() + "]: " + Environment.NewLine);
                Console.WriteLine("Sends: " + Environment.NewLine + fromClient + Environment.NewLine);

                string ToClient = "";

                try
                {

                    ToClient = ParsePacket(fromClient);     // grab the information from the packet string
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Client parse error");
                }

                int length = (ToClient.Length) * sizeof(char);

                Byte[] bytesFormatted = new Byte[length + 2];   // some websocket magic to interpret the packet
                bytesFormatted[0] = 129;
                bytesFormatted[1] = (Byte)length;

                System.Buffer.BlockCopy(ToClient.ToCharArray(), 0, bytesFormatted, 2, length);

                NetworkStream clientStream = theClient.GetStream();
                clientStream.Write(bytesFormatted, 0, bytesFormatted.Length);
                    
                theMutex.ReleaseMutex();

            } while (!new Regex("^9999").IsMatch(fromClient));  // super-secret kill command from client to server

            theMutex.WaitOne();
            Console.WriteLine("Client[" + clientNumber.ToString() + "] has quit" + Environment.NewLine);
            theMutex.ReleaseMutex();
        }


        /// <summary>
        ///     ParsePacket
        ///     This method accepts a raw packet string from the client handler and parses out the pertinent
        ///     information to determine the proper action.
        /// </summary>
        /// <param name="fromClient">raw packet string from client</param>
        /// <returns></returns>
        private string ParsePacket(string fromClient)
        {
            Console.WriteLine(fromClient);

            string cmd = fromClient.Substring(4, 10);

            if (cmd != "ERROR-=-=-")    // No error packet was received...
            {
                if (cmd == "NEW-=-=-=-")    // if the client attempts to connect for the first time
                {
                    int id = System.Convert.ToInt32(fromClient.Substring(0, 4));
                    int red = Int32.Parse( fromClient.Substring(19, 2), System.Globalization.NumberStyles.HexNumber );
                    int green = Int32.Parse( fromClient.Substring(21, 2), System.Globalization.NumberStyles.HexNumber );
                    int blue = Int32.Parse( fromClient.Substring(23, 2), System.Globalization.NumberStyles.HexNumber );

                    Color clr = Color.FromArgb( red, green, blue );
                    Orbs2Revengeance.frmOrbs.NewOrb(id, clr);       // configure a static Orb object to be added by the game loop

                    Console.WriteLine("r, g, b: " + red + ", " + green + ", " + blue);
                }
                else if (cmd == "PULSE=-=-=")   // if the client pulses their orb
                {
                    int id = System.Convert.ToInt32(fromClient.Substring(0, 4));
                    Orbs2Revengeance.frmOrbs.NewPulse(id);      // configure a static OrbPulse struct to be added by the game loop
                }
                else if (cmd == "CATCH=-=-=")   // if the client attempts to catch a critter
                {
                    int id = System.Convert.ToInt32(fromClient.Substring(0, 4));
                    Orbs2Revengeance.frmOrbs.NewCatch(id);      // configure a static Orb id to call the catching method in the game loop
                }
                else if (cmd == "MOVE-=-=-=")   // if the client attempts to navigate the space
                {
                    int id = System.Convert.ToInt32(fromClient.Substring(0, 4));
                    string coords = fromClient.Substring(18);
                    string[] XnY = coords.Split(',');       // grab the coordinates

                    Orbs2Revengeance.frmOrbs.NewMove(id, XnY[0], XnY[1]);   // send the no velocity vector data to the game loop
                }
            }
            else
            {
                // Ignore for the time being...
            }

            return fromClient;
        }

        /// <summary>
        /// public bool Handshake(ref TcpClient client)
        /// This method performs the Handshaking that is required for the websocket protocol.
        /// see http://www.websocket.org/ for more information on websockets.
        /// </summary>
        /// <param name="client"></param>
        /// <returns></returns>
        public bool Handshake(ref TcpClient client)
        {
            bool retVal = false;
            String fromClient = Encoding.UTF8.GetString(ReadFromClient(ref client));
            NetworkStream clientStream = client.GetStream();

            if (new Regex("^GET").IsMatch(fromClient))  // some TCP voodoo for reading the websocket header
            {
                Console.WriteLine("Handshaking...");
                Byte[] response = Encoding.UTF8.GetBytes(
                    "HTTP/1.1 101 Switching Protocols" + Environment.NewLine
                    + "Connection: Upgrade" + Environment.NewLine
                    + "Upgrade: websocket" + Environment.NewLine
                    + "Sec-WebSocket-Accept: " + Convert.ToBase64String(
                        SHA1.Create().ComputeHash(
                            Encoding.UTF8.GetBytes(
                                new Regex("Sec-WebSocket-Key: (.*)").Match(fromClient).Groups[1].Value.Trim() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
                            )
                        )
                    ) + Environment.NewLine
                    + Environment.NewLine);

                clientStream.Write(response, 0, response.Length);

                theMutex.WaitOne();

                // some debug statements to the console for a glimpse under the hood
                Console.WriteLine("Client[" + clientNumber.ToString() + "]: Client handshake" + Environment.NewLine);
                Console.WriteLine(fromClient, Environment.NewLine);
                Console.WriteLine("Client[" + clientNumber.ToString() + "]: Server handshake" + Environment.NewLine);
                Console.WriteLine(Encoding.UTF8.GetString(response));

                if (client.Connected)
                {
                    Console.WriteLine("Client[" + clientNumber.ToString() + "]: Client connected");
                }
                else
                {
                    Console.WriteLine("Client[" + clientNumber.ToString() + "]: Client not connected");
                }

                theMutex.ReleaseMutex();

                retVal = true;
            }


            return retVal;
        }

        /// <summary>
        /// ReadFromClient - Opens the TCP client stream for reading in packet information
        /// </summary>
        /// <param name="client">TCP client connection</param>
        /// <returns>Packet information inside a Byte array</returns>
        public Byte[] ReadFromClient(ref TcpClient client)
        {

            NetworkStream clientStream = client.GetStream();

            theMutex.WaitOne();
            Console.WriteLine("Client[" + clientNumber.ToString() + "]: Waiting for client data...");
            theMutex.ReleaseMutex();

            //server must wait until the data is available from the client
            while (!clientStream.DataAvailable) ;

            theMutex.WaitOne();
            Console.WriteLine("Client[" + clientNumber.ToString() + "]: Found client data..." + Environment.NewLine);
            theMutex.ReleaseMutex();

            Byte[] data = new Byte[client.Available];
            clientStream.Read(data, 0, data.Length);

            return data;
        }

        /// <summary>
        /// DecryptByteArray - Websocket packets are encrypted. This method decrypts the packet for use
        /// </summary>
        /// <param name="dataToBeDecrypted">Encrypted byte array information</param>
        /// <returns>Decrypted byte array information</returns>
        public Byte[] DecryptByteArray(Byte[] dataToBeDecrypted)
        {
            int messageLength = dataToBeDecrypted[1] - 128;
            int keyLength = 6;

            theMutex.WaitOne();
            Console.WriteLine("Client[" + clientNumber.ToString() + "]: Decoding message with length " + messageLength.ToString() + Environment.NewLine);
            theMutex.ReleaseMutex();

            Byte[] decoded = new Byte[messageLength];
            Byte[] key = new Byte[4];

            //key values are embedded in the message at specific offsets into the byte array
            key[0] = dataToBeDecrypted[2];
            key[1] = dataToBeDecrypted[3];
            key[2] = dataToBeDecrypted[4];
            key[3] = dataToBeDecrypted[5];

            for (int i = keyLength; i < messageLength + keyLength; i++)
            {
                decoded[i - keyLength] = (Byte)(dataToBeDecrypted[i] ^ key[(i + keyLength) % 4]);
            }

            return decoded;
        }
    }
}